package vulnerabilityreport

import (
	"fmt"
	"strings"
	"time"

	batchv1 "k8s.io/api/batch/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/validation"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1"
	"github.com/aquasecurity/trivy-operator/pkg/docker"
	"github.com/aquasecurity/trivy-operator/pkg/kube"
	"github.com/aquasecurity/trivy-operator/pkg/trivyoperator"
	"github.com/aquasecurity/trivy-operator/pkg/utils"
)

type ScanJobBuilder struct {
	plugin                   Plugin
	pluginContext            trivyoperator.PluginContext
	ttl                      *time.Duration
	scanSecretTTL            *time.Duration
	timeout                  time.Duration
	object                   client.Object
	credentials              map[string]docker.Auth
	affinity                 *corev1.Affinity
	tolerations              []corev1.Toleration
	nodeSelector             map[string]string
	annotations              map[string]string
	podTemplateLabels        map[string]string
	podSecurityContext       *corev1.PodSecurityContext
	containerSecurityContext *corev1.SecurityContext
	podPriorityClassName     string
	skipInitContainers       bool
	sbomClusterReports       map[string]v1alpha1.SbomReportData
	customVolumesMount       []corev1.VolumeMount
	customVolumes            []corev1.Volume
}

func NewScanJobBuilder() *ScanJobBuilder {
	return &ScanJobBuilder{}
}

func (s *ScanJobBuilder) WithPlugin(plugin Plugin) *ScanJobBuilder {
	s.plugin = plugin
	return s
}

func (s *ScanJobBuilder) WithPluginContext(pluginContext trivyoperator.PluginContext) *ScanJobBuilder {
	s.pluginContext = pluginContext
	return s
}

func (s *ScanJobBuilder) WithTimeout(timeout time.Duration) *ScanJobBuilder {
	s.timeout = timeout
	return s
}

func (s *ScanJobBuilder) WithTTL(ttl *time.Duration) *ScanJobBuilder {
	s.ttl = ttl
	return s
}

func (s *ScanJobBuilder) WithScanSecretTTL(ttl *time.Duration) *ScanJobBuilder {
	s.scanSecretTTL = ttl
	return s
}

func (s *ScanJobBuilder) WithSkipInitContainers(skipInitContainers bool) *ScanJobBuilder {
	s.skipInitContainers = skipInitContainers
	return s
}

func (s *ScanJobBuilder) WithObject(object client.Object) *ScanJobBuilder {
	s.object = object
	return s
}

func (s *ScanJobBuilder) WithAffinity(affinity *corev1.Affinity) *ScanJobBuilder {
	s.affinity = affinity
	return s
}

func (s *ScanJobBuilder) WithTolerations(tolerations []corev1.Toleration) *ScanJobBuilder {
	s.tolerations = tolerations
	return s
}

func (s *ScanJobBuilder) WithAnnotations(annotations map[string]string) *ScanJobBuilder {
	s.annotations = annotations
	return s
}

func (s *ScanJobBuilder) WithNodeSelector(nodeSelector map[string]string) *ScanJobBuilder {
	s.nodeSelector = nodeSelector
	return s
}

func (s *ScanJobBuilder) WithPodSecurityContext(podSecurityContext *corev1.PodSecurityContext) *ScanJobBuilder {
	s.podSecurityContext = podSecurityContext
	return s
}

func (s *ScanJobBuilder) WithPodPriorityClassName(podPriorityClassName string) *ScanJobBuilder {
	s.podPriorityClassName = podPriorityClassName
	return s
}

func (s *ScanJobBuilder) WithSecurityContext(securityContext *corev1.SecurityContext) *ScanJobBuilder {
	s.containerSecurityContext = securityContext
	return s
}

func (s *ScanJobBuilder) WithPodTemplateLabels(podTemplateLabels map[string]string) *ScanJobBuilder {
	s.podTemplateLabels = podTemplateLabels
	return s
}

func (s *ScanJobBuilder) WithCredentials(credentials map[string]docker.Auth) *ScanJobBuilder {
	s.credentials = credentials
	return s
}

func (s *ScanJobBuilder) WithSbomClusterReports(sbomClusterReports map[string]v1alpha1.SbomReportData) *ScanJobBuilder {
	s.sbomClusterReports = sbomClusterReports
	return s
}

func (s *ScanJobBuilder) WithCustomVolumes(customVolumes []corev1.Volume) *ScanJobBuilder {
	s.customVolumes = customVolumes
	return s
}

func (s *ScanJobBuilder) WithCustomVolumesMount(customVolumesMount []corev1.VolumeMount) *ScanJobBuilder {
	s.customVolumesMount = customVolumesMount
	return s
}

func (s *ScanJobBuilder) Get() (*batchv1.Job, []*corev1.Secret, error) {
	spec, err := kube.GetPodSpec(s.object)
	if err != nil {
		return nil, nil, err
	}

	templateSpec, secrets, err := s.plugin.GetScanJobSpec(s.pluginContext, s.object, s.credentials, s.containerSecurityContext, s.sbomClusterReports)
	if err != nil {
		return nil, nil, err
	}

	if s.affinity != nil {
		templateSpec.Affinity = s.affinity
	}

	templateSpec.Tolerations = append(templateSpec.Tolerations, s.tolerations...)
	if s.podSecurityContext != nil {
		templateSpec.SecurityContext = s.podSecurityContext
	}

	if len(s.customVolumes) > 0 {
		templateSpec.Volumes = append(templateSpec.Volumes, s.customVolumes...)
	}
	if len(s.customVolumesMount) > 0 {
		for i := range templateSpec.Containers {
			templateSpec.Containers[i].VolumeMounts = append(templateSpec.Containers[i].VolumeMounts, s.customVolumesMount...)
		}
		for i := range templateSpec.InitContainers {
			templateSpec.InitContainers[i].VolumeMounts = append(templateSpec.InitContainers[i].VolumeMounts, s.customVolumesMount...)
		}
	}
	templateSpec.PriorityClassName = s.podPriorityClassName

	templateSpec.NodeSelector = s.nodeSelector

	containerImagesAsJSON, err := kube.GetContainerImagesFromPodSpec(spec, s.skipInitContainers).AsJSON()
	if err != nil {
		return nil, nil, err
	}

	podSpecHash := kube.ComputeHash(spec)

	jobLabels := map[string]string{
		trivyoperator.LabelResourceSpecHash:           podSpecHash,
		trivyoperator.LabelK8SAppManagedBy:            trivyoperator.AppTrivyOperator,
		trivyoperator.LabelVulnerabilityReportScanner: s.pluginContext.GetName(),
	}
	if len(s.sbomClusterReports) > 0 {
		jobLabels[trivyoperator.LabelReusedReport] = "true"
	}

	podTemplateLabels := make(map[string]string)
	for k, v := range jobLabels {
		podTemplateLabels[k] = v
	}
	for k, v := range s.podTemplateLabels {
		podTemplateLabels[k] = v
	}

	jobSpec := batchv1.JobSpec{
		BackoffLimit:          ptr.To[int32](0),
		Completions:           ptr.To[int32](1),
		ActiveDeadlineSeconds: utils.DurationSecondsPtr(s.timeout),
		Template: corev1.PodTemplateSpec{
			ObjectMeta: metav1.ObjectMeta{
				Labels:      podTemplateLabels,
				Annotations: s.annotations,
			},
			Spec: templateSpec,
		},
	}

	if s.ttl != nil {
		if s.ttl.Seconds() > 0 {
			jobSpec.TTLSecondsAfterFinished = ptr.To[int32](int32(s.ttl.Seconds()))
		}
	}

	jobAnnotations := make(map[string]string, len(s.annotations)+1)
	jobAnnotations[trivyoperator.AnnotationContainerImages] = containerImagesAsJSON
	for k, v := range s.annotations {
		jobAnnotations[k] = v
	}
	job := &batchv1.Job{
		ObjectMeta: metav1.ObjectMeta{
			Name:        GetScanJobName(s.object),
			Namespace:   s.pluginContext.GetNamespace(),
			Labels:      jobLabels,
			Annotations: jobAnnotations,
		},
		Spec: jobSpec,
	}
	var secretLabels map[string]string
	if s.scanSecretTTL != nil {
		secretLabels = map[string]string{
			trivyoperator.LabelK8SAppManagedBy: trivyoperator.AppTrivyOperator,
			v1alpha1.TTLSecretAnnotation:       s.scanSecretTTL.String(),
		}
	}
	// secrets will be created with scan jobs in same namespace where scan job will run
	for i := range secrets {
		secrets[i].Namespace = s.pluginContext.GetNamespace()
		secrets[i].Labels = secretLabels
	}
	s.updateScanJobForWorkloadNamespace(job, spec, secrets)

	err = kube.ObjectToObjectMeta(s.object, &job.ObjectMeta)
	if err != nil {
		return nil, nil, err
	}

	err = kube.ObjectToObjectMeta(s.object, &job.Spec.Template.ObjectMeta)
	if err != nil {
		return nil, nil, err
	}

	return job, secrets, nil
}

// When run scan job in workload namespace is enabled then this method will update scanjob spec with these changes
// - namespace same as workload
// - service account same as workload service account
// - ImagePullSecret same as workload imagePullSecret
func (s *ScanJobBuilder) updateScanJobForWorkloadNamespace(job *batchv1.Job, podspec corev1.PodSpec, secrets []*corev1.Secret) {
	operatorConfig := s.pluginContext.GetTrivyOperatorConfig()
	if !operatorConfig.VulnerabilityScanJobsInSameNamespace() {
		return
	}
	workloadNamespace := s.object.GetNamespace()
	// Only update job and secret namespaces if workload has a valid namespace
	if workloadNamespace != "" {
		job.Namespace = workloadNamespace
		for i := range secrets {
			secrets[i].Namespace = workloadNamespace
		}
	}
	job.Spec.Template.Spec.ServiceAccountName = podspec.ServiceAccountName
	job.Spec.Template.Spec.ImagePullSecrets = podspec.ImagePullSecrets
}

func GetScanJobName(obj client.Object) string {
	return fmt.Sprintf("scan-vulnerabilityreport-%s", kube.ComputeHash(kube.ObjectRef{
		Kind:      kube.Kind(obj.GetObjectKind().GroupVersionKind().Kind),
		Namespace: obj.GetNamespace(),
		Name:      obj.GetName(),
	}))
}

func RegistryCredentialsSecretName(obj client.Object) string {
	return fmt.Sprintf("%s-regcred", GetScanJobName(obj))
}

type ReportBuilder struct {
	scheme                  *runtime.Scheme
	controller              client.Object
	container               string
	hash                    string
	data                    v1alpha1.VulnerabilityReportData
	reportTTL               *time.Duration
	resourceLabelsToInclude []string
	additionalReportLabels  labels.Set
}

func NewReportBuilder(scheme *runtime.Scheme) *ReportBuilder {
	return &ReportBuilder{
		scheme: scheme,
	}
}

func (b *ReportBuilder) Controller(controller client.Object) *ReportBuilder {
	b.controller = controller
	return b
}

func (b *ReportBuilder) Container(name string) *ReportBuilder {
	b.container = name
	return b
}

func (b *ReportBuilder) PodSpecHash(hash string) *ReportBuilder {
	b.hash = hash
	return b
}

func (b *ReportBuilder) Data(data v1alpha1.VulnerabilityReportData) *ReportBuilder {
	b.data = data
	return b
}

func (b *ReportBuilder) ReportTTL(ttl *time.Duration) *ReportBuilder {
	b.reportTTL = ttl
	return b
}

func (b *ReportBuilder) ResourceLabelsToInclude(resourceLabelsToInclude []string) *ReportBuilder {
	b.resourceLabelsToInclude = resourceLabelsToInclude
	return b
}

func (b *ReportBuilder) AdditionalReportLabels(additionalReportLabels map[string]string) *ReportBuilder {
	b.additionalReportLabels = additionalReportLabels
	return b
}

func (b *ReportBuilder) reportName() string {
	kind := b.controller.GetObjectKind().GroupVersionKind().Kind
	name := b.controller.GetName()
	reportName := fmt.Sprintf("%s-%s-%s", strings.ToLower(kind), name, b.container)
	if len(validation.IsValidLabelValue(reportName)) == 0 {
		return reportName
	}

	return fmt.Sprintf("%s-%s", strings.ToLower(kind), kube.ComputeHash(name+"-"+b.container))
}

func (b *ReportBuilder) Get() (*v1alpha1.VulnerabilityReport, *v1alpha1.ClusterVulnerabilityReport, error) {
	var nvr *v1alpha1.VulnerabilityReport
	var cvr *v1alpha1.ClusterVulnerabilityReport
	var err error
	n := b.controller.GetNamespace()
	if n != "" {
		nvr, err = b.namespacedVulnerabilityReport()
		if err != nil {
			return nil, nil, err
		}
	} else {
		cvr, err = b.clusterVulnerabilityReport()
		if err != nil {
			return nil, nil, err
		}
	}
	return nvr, cvr, nil
}

func (b *ReportBuilder) namespacedVulnerabilityReport() (*v1alpha1.VulnerabilityReport, error) {
	reportLabels := map[string]string{
		trivyoperator.LabelContainerName:   b.container,
		trivyoperator.LabelK8SAppManagedBy: trivyoperator.AppTrivyOperator,
	}

	if b.hash != "" {
		reportLabels[trivyoperator.LabelResourceSpecHash] = b.hash
	}
	// append matching resource labels by config to report
	kube.AppendResourceLabels(b.resourceLabelsToInclude, b.controller.GetLabels(), reportLabels)
	// append custom labels by config to report
	kube.AppendCustomLabels(b.additionalReportLabels, reportLabels)

	report := v1alpha1.VulnerabilityReport{
		ObjectMeta: metav1.ObjectMeta{
			Name:      b.reportName(),
			Namespace: b.controller.GetNamespace(),
			Labels:    reportLabels,
		},
		Report: b.data,
	}

	if b.reportTTL != nil {
		report.Annotations = map[string]string{
			v1alpha1.TTLReportAnnotation: b.reportTTL.String(),
		}
	}
	err := kube.ObjectToObjectMeta(b.controller, &report.ObjectMeta)
	if err != nil {
		return nil, err
	}
	err = controllerutil.SetControllerReference(b.controller, &report, b.scheme)
	if err != nil {
		return nil, fmt.Errorf("setting controller reference: %w", err)
	}
	// The OwnerReferencesPermissionsEnforcement admission controller protects the
	// access to metadata.ownerReferences[x].blockOwnerDeletion of an object, so
	// that only users with "update" permission to the finalizers subresource of the
	// referenced owner can change it.
	// We set metadata.ownerReferences[x].blockOwnerDeletion to false so that
	// additional RBAC permissions are not required when the OwnerReferencesPermissionsEnforcement
	// is enabled.
	// See https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#ownerreferencespermissionenforcement
	report.OwnerReferences[0].BlockOwnerDeletion = ptr.To[bool](false)
	return &report, nil
}

func (b *ReportBuilder) clusterVulnerabilityReport() (*v1alpha1.ClusterVulnerabilityReport, error) {
	reportLabels := map[string]string{
		trivyoperator.LabelContainerName:   b.container,
		trivyoperator.LabelK8SAppManagedBy: trivyoperator.AppTrivyOperator,
	}

	if b.hash != "" {
		reportLabels[trivyoperator.LabelResourceSpecHash] = b.hash
	}
	// append matching resource labels by config to report
	kube.AppendResourceLabels(b.resourceLabelsToInclude, b.controller.GetLabels(), reportLabels)
	// append custom labels by config to report
	kube.AppendCustomLabels(b.additionalReportLabels, reportLabels)

	report := v1alpha1.ClusterVulnerabilityReport{
		ObjectMeta: metav1.ObjectMeta{
			Name:   b.reportName(),
			Labels: reportLabels,
		},
		Report: b.data,
	}

	if b.reportTTL != nil {
		report.Annotations = map[string]string{
			v1alpha1.TTLReportAnnotation: b.reportTTL.String(),
		}
	}
	err := kube.ObjectToObjectMeta(b.controller, &report.ObjectMeta)
	if err != nil {
		return nil, err
	}
	err = controllerutil.SetControllerReference(b.controller, &report, b.scheme)
	if err != nil {
		return nil, fmt.Errorf("setting controller reference: %w", err)
	}
	// The OwnerReferencesPermissionsEnforcement admission controller protects the
	// access to metadata.ownerReferences[x].blockOwnerDeletion of an object, so
	// that only users with "update" permission to the finalizers subresource of the
	// referenced owner can change it.
	// We set metadata.ownerReferences[x].blockOwnerDeletion to false so that
	// additional RBAC permissions are not required when the OwnerReferencesPermissionsEnforcement
	// is enabled.
	// See https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#ownerreferencespermissionenforcement
	report.OwnerReferences[0].BlockOwnerDeletion = ptr.To[bool](false)
	return &report, nil
}
